package net.sudot.chess.util;

import com.google.zxing.BarcodeFormat;
import com.google.zxing.Binarizer;
import com.google.zxing.BinaryBitmap;
import com.google.zxing.DecodeHintType;
import com.google.zxing.EncodeHintType;
import com.google.zxing.LuminanceSource;
import com.google.zxing.MultiFormatReader;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.NotFoundException;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.common.HybridBinarizer;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.ImageObserver;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.EnumMap;
import java.util.Map;

/**
 * 二维码生成工具
 *
 * @author tangjialin on 2018-03-22.
 */
public class QrcodeUtils {
    /** 生成二维码或一维码的格式 */
    private static final String FORMAT = "jpg";
    /** 生成条形码的默认宽度,及二维码的默认边长 */
    private static final int DEFAULT_WIDTH = 600;
    /** 生成条形码的默认高度 */
    private static final int DEFAULT_HIEGHT = 80;
    /** 二维码中头像占比 */
    private static final int DEFAULT_LOGOPART = 6;
    /** 二维码中头像边框线宽度 */
    private static final int DEFAULT_BORDER = 4;
    private static final int BLACK = 0xFF000000;
    private static final int WHITE = 0xFFFFFFFF;

    private static final Logger logger = LoggerFactory.getLogger(QrcodeUtils.class);

    /**
     * 创建生成默认高度(DEFAULT_WIDTH)的二维码图片
     * 可以指定是否带logo
     *
     * @param content    二维码内容
     * @param logoStream logo 图片流对象，可以为空
     * @return 二维码图片的字节数组
     */
    public static byte[] createQrcode(String content, InputStream logoStream) {
        return createQrcode(content, DEFAULT_WIDTH, logoStream);
    }

    /**
     * 根据指定边长创建生成的二维码
     * 可以指定是否带logo
     *
     * @param content    二维码内容
     * @param length     二维码的高度和宽度
     * @param logoStream logo 图片流对象，可以为空
     * @return 二维码图片的字节数组
     */
    public static byte[] createQrcode(String content, int length, InputStream logoStream) {
        BitMatrix qrCodeMatrix = createQrcodeMatrix(content, length);
        if (qrCodeMatrix == null) { return null; }
        try {
            BufferedImage image = toBufferedImage(qrCodeMatrix);
            ByteArrayOutputStream output = new ByteArrayOutputStream();
            if (logoStream != null) {
                // 添加logo图片
                image = overlapImage(image, ImageIO.read(logoStream));
            }
            ImageIO.write(image, FORMAT, output);
            return output.toByteArray();
        } catch (Exception e) {
            logger.warn("内容为：【" + content + "】的二维码生成失败！", e);
            return null;
        }
    }

    /**
     * 创建生成默认(DEFAULT_WIDTH * DEFAULT_HIEGHT)的条形码图片
     * 图片下方显示条形码内容
     *
     * @param content 条形码内容
     * @return 二维码图片的字节数组
     */
    public static byte[] createBarcode(String content) {
        return createBarcode(content, DEFAULT_WIDTH, DEFAULT_HIEGHT, true);
    }

    /**
     * 创建生成默认(DEFAULT_WIDTH * DEFAULT_HIEGHT)的条形码图片
     *
     * @param content     条形码内容
     * @param displayCode 是否显示条形码内容
     * @return 二维码图片的字节数组
     */
    public static byte[] createBarcode(String content, boolean displayCode) {
        return createBarcode(content, DEFAULT_WIDTH, DEFAULT_HIEGHT, displayCode);
    }

    /**
     * 根据指定宽度和高度创建生成的条形码图片
     * 图片下方显示条形码内容
     *
     * @param content 条形码内容
     * @param width   条形码宽度
     * @param height  条形码高度
     * @return 条形码图片的字节数组
     */
    public static byte[] createBarcode(String content, int width, int height) {
        return createBarcode(content, width, height, true);
    }

    /**
     * 根据指定宽度和高度创建生成的条形码图片
     *
     * @param content     条形码内容
     * @param width       条形码宽度
     * @param height      条形码高度
     * @param displayCode 是否显示条形码内容
     * @return 条形码图片的字节数组
     */
    public static byte[] createBarcode(String content, int width, int height, boolean displayCode) {
        BitMatrix barCodeMatrix = createBarCodeMatrix(content, width, height);
        if (barCodeMatrix == null) { return null; }
        BufferedImage image = toBufferedImage(barCodeMatrix);
        ByteArrayOutputStream output = new ByteArrayOutputStream();
        try {
            if (displayCode) {
                // 添加条形码内容
                image = overlapImage(image, content);
            }
            ImageIO.write(image, FORMAT, output);
            return output.toByteArray();
        } catch (Exception e) {
            logger.warn("内容为：【" + content + "】的条形码生成失败！", e);
        }
        return null;
    }

    /**
     * 解析二维码
     *
     * @param input 二维码文件内容
     * @return 二维码的内容
     */
    public static String decodeQrcode(InputStream input) throws IOException, NotFoundException {
        BufferedImage image = ImageIO.read(input);
        return decodeQrcode(image);
    }

    /**
     * 解析二维码
     *
     * @param image 二维码文件内容
     * @return 二维码的内容
     */
    public static String decodeQrcode(BufferedImage image) throws IOException, NotFoundException {
        LuminanceSource source = new BufferedImageLuminanceSource(image);
        Binarizer binarizer = new HybridBinarizer(source);
        BinaryBitmap binaryBitmap = new BinaryBitmap(binarizer);
        Map<DecodeHintType, Object> hints = new EnumMap<>(DecodeHintType.class);
        hints.put(DecodeHintType.CHARACTER_SET, StandardCharsets.UTF_8.name());
        return new MultiFormatReader().decode(binaryBitmap, hints).getText();
    }

    /**
     * 解析条形码
     *
     * @param input 条形码文件内容
     * @return 条形码的内容
     */
    public static String decodeBarcode(InputStream input) throws IOException, NotFoundException {
        BufferedImage image = ImageIO.read(input);
        return decodeBarcode(image);
    }

    /**
     * 解析条形码
     *
     * @param image 条形码文件内容
     * @return 条形码的内容
     */
    public static String decodeBarcode(BufferedImage image) throws IOException, NotFoundException {
        LuminanceSource source = new BufferedImageLuminanceSource(image);
        BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
        return new MultiFormatReader().decode(bitmap, null).getText();
    }

    /**
     * 根据内容生成二维码数据
     *
     * @param content 二维码文字内容[为了信息安全性，一般都要先进行数据加密]
     * @param length  二维码图片宽度和高度
     * @return 返回其像素信息
     */
    private static BitMatrix createQrcodeMatrix(String content, int length) {
        Map<EncodeHintType, Object> hints = new EnumMap<>(EncodeHintType.class);
        // 设置字符编码
        hints.put(EncodeHintType.CHARACTER_SET, StandardCharsets.UTF_8.name());
        // 指定纠错等级
        hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H);

        try {
            return new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, length, length, hints);
        } catch (Exception e) {
            logger.warn("内容为：【" + content + "】的二维码生成失败！", e);
            return null;
        }
    }

    /**
     * 根据内容生成条形码数据
     *
     * @param content 条形码文字内容[为了信息安全性，一般都要先进行数据加密]
     * @param width   条形码图片宽度
     * @param height  条形码图片高度
     * @return 返回其像素信息
     */
    private static BitMatrix createBarCodeMatrix(String content, int width, int height) {
        Map<EncodeHintType, Object> hints = new EnumMap<>(EncodeHintType.class);
        // 设置字符编码
        hints.put(EncodeHintType.CHARACTER_SET, StandardCharsets.UTF_8.name());
        // 指定纠错等级
        hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H);
        try {
            return new MultiFormatWriter().encode(content, BarcodeFormat.CODE_128, width, height, hints);
        } catch (Exception e) {
            logger.warn("内容为：【" + content + "】的条形码生成失败！", e);
            return null;
        }
    }

    /**
     * 将logo添加到二维码中间
     *
     * @param image     生成的二维码图片对象
     * @param logoImage logo图片对象
     */
    private static BufferedImage overlapImage(BufferedImage image, BufferedImage logoImage) throws IOException {
        try {
            Graphics2D g = image.createGraphics();
            // 考虑到logo图片贴到二维码中，建议大小不要超过二维码的1/5;
            int width = image.getWidth() / DEFAULT_LOGOPART;
            int height = image.getHeight() / DEFAULT_LOGOPART;
            // logo起始位置，此目的是为logo居中显示
            int x = (image.getWidth() - width) / 2;
            int y = (image.getHeight() - height) / 2;
            // 写入logo图片到二维码
            g.drawImage(logoImage, x, y, width, height, null);

            // 给logo画边框
            // 构造一个具有指定线条宽度以及 cap 和 join 风格的默认值的实心 BasicStroke
            int border = DEFAULT_BORDER;
            g.setStroke(new BasicStroke(border));
            g.setColor(Color.WHITE);
            // 绘制logo边框并设置边角弧度,使其为圆角矩形
            g.drawRoundRect(x - (border / 2), y - (border / 2), width + border, height + border, 20, 20);

            g.dispose();
            return image;
        } catch (Exception e) {
            throw new IOException("二维码添加logo时发生异常！", e);
        }
    }

    /**
     * 合并条形码下方显示内容
     *
     * @param image   生成的条形码图片对象
     * @param content 条形码内容图片
     * @return 返回合并后的条形码图片
     */
    private static BufferedImage overlapImage(BufferedImage image, String content) throws IOException {
        try {
            // 构造文字并计算文字在图片中占用的宽和高
            Font font = new Font("微软雅黑", Font.BOLD, 27);
            FontMetrics metrics = new FontMetrics(font) {
            };
            Rectangle2D bounds = metrics.getStringBounds(content, null);
            int textWidth = (int) bounds.getWidth();
            int textHeight = (int) bounds.getHeight();

            int width = image.getWidth();
            int height = image.getHeight() + textHeight * 2;
            BufferedImage outImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
            Graphics2D g = outImage.createGraphics();
            g.setBackground(Color.WHITE);
            g.clearRect(0, 0, width, height);
            g.drawImage(image, 0, textHeight / 2, null);
            g.setColor(Color.BLACK);
            g.setFont(font);

            g.drawString(content, (width - textWidth) / 2, height - textHeight / 2);
            g.dispose();
            return outImage;
        } catch (Exception e) {
            throw new IOException("条形码添加内容时发生异常！", e);
        }
    }

    /**
     * 将生成的像素点转换成图片
     *
     * @param matrix 像素点信息
     * @return 返回其图片
     */
    private static BufferedImage toBufferedImage(BitMatrix matrix) {
        int width = matrix.getWidth();
        int height = matrix.getHeight();
        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        for (int x = 0; x < width; x++) {
            for (int y = 0; y < height; y++) {
                image.setRGB(x, y, matrix.get(x, y) ? BLACK : WHITE);
            }
        }

        return image;
    }

    private static final class BufferedImageLuminanceSource extends LuminanceSource {
        private final BufferedImage image;
        private final int left;
        private final int top;

        public BufferedImageLuminanceSource(BufferedImage image) {
            this(image, 0, 0, image.getWidth(), image.getHeight());
        }

        public BufferedImageLuminanceSource(BufferedImage image, int left, int top, int width, int height) {
            super(width, height);
            int sourceWidth = image.getWidth();
            int sourceHeight = image.getHeight();
            if (left + width <= sourceWidth && top + height <= sourceHeight) {
                for (int y = top; y < top + height; ++y) {
                    for (int x = left; x < left + width; ++x) {
                        if ((image.getRGB(x, y) & -16777216) == 0) {
                            image.setRGB(x, y, -1);
                        }
                    }
                }

                this.image = new BufferedImage(sourceWidth, sourceHeight, 10);
                this.image.getGraphics().drawImage(image, 0, 0, (ImageObserver) null);
                this.left = left;
                this.top = top;
            } else {
                throw new IllegalArgumentException("Crop rectangle does not fit within image data.");
            }
        }

        @Override
        public byte[] getRow(int y, byte[] row) {
            if (y >= 0 && y < this.getHeight()) {
                int width = this.getWidth();
                if (row == null || row.length < width) {
                    row = new byte[width];
                }

                this.image.getRaster().getDataElements(this.left, this.top + y, width, 1, row);
                return row;
            } else {
                throw new IllegalArgumentException("Requested row is outside the image: " + y);
            }
        }

        @Override
        public byte[] getMatrix() {
            int width = this.getWidth();
            int height = this.getHeight();
            int area = width * height;
            byte[] matrix = new byte[area];
            this.image.getRaster().getDataElements(this.left, this.top, width, height, matrix);
            return matrix;
        }

        @Override
        public boolean isCropSupported() {
            return true;
        }

        @Override
        public LuminanceSource crop(int left, int top, int width, int height) {
            return new BufferedImageLuminanceSource(this.image, this.left + left, this.top + top, width, height);
        }

        @Override
        public boolean isRotateSupported() {
            return true;
        }

        @Override
        public LuminanceSource rotateCounterClockwise() {
            int sourceWidth = this.image.getWidth();
            int sourceHeight = this.image.getHeight();
            AffineTransform transform = new AffineTransform(0.0D, -1.0D, 1.0D, 0.0D, 0.0D, (double) sourceWidth);
            BufferedImage rotatedImage = new BufferedImage(sourceHeight, sourceWidth, 10);
            Graphics2D g = rotatedImage.createGraphics();
            g.drawImage(this.image, transform, (ImageObserver) null);
            g.dispose();
            int width = this.getWidth();
            return new BufferedImageLuminanceSource(rotatedImage, this.top, sourceWidth - (this.left + width), this.getHeight(), width);
        }
    }
}
